Automated tests are important for most apps.
In this article, we’ll take a look at how to write tests for React components.
Timers
We can mock timers in our React component tests.
For example, if we have:
import React, { useEffect } from "react";
export default function Card({ onSelect }) {
useEffect(() => {
const timeoutID = setTimeout(() => {
onSelect(null);
}, 5000);
return () => {
clearTimeout(timeoutID);
};
}, [onSelect]);
return (
<>
<button
data-testid='button'
onClick={() => onSelect(1)}
>
button
</button>
</>
);
}
Then we can test it by writing the following:
Card.test.js
import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";
import Card from "./card";
jest.useFakeTimers();
let container = null;
beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
});
afterEach(() => {
unmountComponentAtNode(container);
container.remove();
container = null;
});
it("should select null after timing out", () => {
const onSelect = jest.fn();
act(() => {
render(<Card onSelect={onSelect} />, container);
});
act(() => {
jest.advanceTimersByTime(100);
});
expect(onSelect).not.toHaveBeenCalled();
act(() => {
jest.advanceTimersByTime(5000);
});
expect(onSelect).toHaveBeenCalledWith(null);
});
it("should clean up on being removed", () => {
const onSelect = jest.fn();
act(() => {
render(<Card onSelect={onSelect} />, container);
});
act(() => {
render(null, container);
});
act(() => {
jest.advanceTimersByTime(5000);
});
expect(onSelect).not.toHaveBeenCalled();
});
it("should accept selections", () => {
const onSelect = jest.fn();
act(() => {
render(<Card onSelect={onSelect} />, container);
});
act(() => {
container
.querySelector("[data-testid='button']")
.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(onSelect).toHaveBeenCalledWith(1);
});
We have the usual beforeEach
and afterEach
hooks to create and remove the container for mounting our component for testing.
In the ‘should select null after timing out’, we set the timer to the time we want by calling the jest.advanceTimerByTime
method.
Then we check what the mockedonSelect
function is called with toHaveBeenCalledWith
.
And we check if onSelect
is called with toHaveBeenCalled
.
In the “should clean up on being removed” test, we make sure that onSelect
isn’t called after it’s unmounted.
In the “should accept selections” test, we triggered the click event on the button.
Then we check is onSelect
is called with the argument we expect.
Snapshot Testing
We can test snapshots, which is the rendered component at a given time.
For example, if we have:
import React from "react";
export default function Hello({ name }) {
return (
<>
<p>hello {name}</p>
</>
);
}
Then we can add the following test to test the component:
Hello.test.js
import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";
import pretty from "pretty";
import Hello from "./hello";
let container = null;
beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
});
afterEach(() => {
unmountComponentAtNode(container);
container.remove();
container = null;
});
it("should render a greeting", () => {
act(() => {
render(<Hello />, container);
});
expect(pretty(container.innerHTML)).toMatchInlineSnapshot(`"<p>hello </p>"`);
act(() => {
render(<Hello name="james" />, container);
});
expect(pretty(container.innerHTML)).toMatchInlineSnapshot(
`"<p>hello james</p>"`
);
});
We have the same beforeEach
and afterEach
hooks as before.
To test the component, we get the rendered HTML with container.innerHTML
.
Then we check it against the HTML code with toMatchInlineSnapshot
.
We have to install pretty
and prettier
by running:
npm i pretty prettier --save-dev
to get the pretty
function to render the HTML.
Conclusion
We can mock timers and test React components with rendered HTML.